package ar.uba.fi.tonyvaliente.tree;

import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import ar.uba.fi.tonyvaliente.files.File;
import ar.uba.fi.tonyvaliente.files.FileException;
import ar.uba.fi.tonyvaliente.files.Persistor;
import ar.uba.fi.tonyvaliente.files.RecordPosition;

/**
 * Implementación en memoria del árbol.
 * 
 * @author santiago
 *
 */
public class DiskBPlusTree<K extends Comparable<K>, V> extends BPlusTree<K, V> {

	private static Log log = LogFactory.getLog(DiskBPlusTree.class);
	
	File file;
	BPlusTreeFileWrapper<K,V> wrapper;

	public DiskBPlusTree(Integer elementosPorNodo, String pathName,
			Persistor<K> keyPersistor, Persistor<V> valuePersistor) throws BPlusTreeException {
		
		super();
		try {
			this.file = File.create(pathName, 4096);
			this.wrapper = new BPlusTreeFileWrapper<K,V>(file, keyPersistor, valuePersistor);
		} catch (FileException e) {
			log.fatal("Error creando el BPlusTree", e);
			throw new BPlusTreeException("Error creando el BPlusTree", e);
		}

		try{
			Reference<K, V> reference = this.createReference();
			this.setElementosPorNodo(elementosPorNodo);
			reference.setNode(new InnerBPlusTreeNode<K, V>(this, elementosPorNodo));
			this.setRoot(reference);
		}catch(BPlusTreeException e){
			log.fatal("Se produjo un error creando la raiz del árbol", e);
			throw e;
		}
	}

	public DiskBPlusTree(String pathName,boolean readOnly,
			Persistor<K> keyPersistor, Persistor<V> valuePersistor) throws BPlusTreeException {
		
		super();
		try {
			this.file = File.open(pathName, readOnly);
			this.wrapper = new BPlusTreeFileWrapper<K,V>(file, keyPersistor, valuePersistor);
		} catch (FileException e) {
			log.fatal("Error abriendo el BPlusTree" , e);
			throw new BPlusTreeException("Error abriendo el BPlusTree, path: " + pathName, e);
		}

		try{
			this.getRoot();
		}catch(BPlusTreeException e){
			log.fatal("Se produjo un error obteniendo la raiz del árbol", e);
			throw e;
		}
	}


	/* (non-Javadoc)
	 * @see ar.uba.fi.datos.tonyvaliente.tree.BPlusTree#createReference()
	 */
	@Override
	protected Reference<K, V> createReference() {

		Reference<K, V> reference = new DiskReference<K, V>(this.wrapper);
		return reference;
	}

	/* (non-Javadoc)
	 * @see ar.uba.fi.datos.tonyvaliente.tree.BPlusTree#get(java.lang.Comparable)
	 */
	@Override
	public V get(K key) throws BPlusTreeException, NoSuchElementException {
		return this.getRoot().getNode().get(key);
	}



	@Override
	protected Reference<K, V> getRoot() throws BPlusTreeException{
		if(super.getRoot() == null){
			
			try {
				DiskReference<K,V> rootReference = new DiskReference<K, V>(this.wrapper);
				RecordPosition rootPosition;
				rootPosition = this.wrapper.getRootNodePosition();
				BPlusTreeNode<K, V> rootNode = this.wrapper.getNode(rootPosition.getBlockNumber(), rootPosition.getOffset());
				rootReference.setNode(rootNode);
				rootReference.setRecordPosition(rootPosition);
				// TODO Esto es solo un parche, la verdadera solución sería guardar más datos del árbol.
				this.setElementosPorNodo(rootNode.getMaxCantidadElementos());
				super.setRoot(rootReference);
			} catch (BPlusTreeException e) {
				log.fatal("Error obteniendo la raiz del árbol", e);
				throw e;
			}
		}
		return super.getRoot();
	}



	@Override
	protected void setRoot(Reference<K, V> root) throws BPlusTreeException{
		super.setRoot(root);
		DiskReference<K, V> diskRoot = (DiskReference<K, V>)root;
		try {
//			if(diskRoot.getRecordPosition() == null){
				RecordPosition recordPosition = this.wrapper.appendNode(diskRoot.getNode());
				this.wrapper.setRootNodePosition(recordPosition);
				diskRoot.setRecordPosition(recordPosition);
//			}else{
//				this.wrapper.setRootNodePosition(diskRoot.getRecordPosition());
//				diskRoot.updateNode();
//			}
		} catch (BPlusTreeException e) {
			log.fatal("Error seteando la raiz del árbol", e);
			throw e;
		}
		
	}


	public Reference<K, V> getFirst() throws BPlusTreeException{
		Reference<K, V> primero = (DiskReference<K, V>) super.getFirst();
		RecordPosition posicionPrimero = null;
		if(primero != null){
			posicionPrimero = ((DiskReference<K, V>) primero).getRecordPosition();
		}else{
			posicionPrimero = this.wrapper.getFirstNodePosition();
			primero = this.createReference();
		}
		
		if(posicionPrimero != null){
			BPlusTreeNode<K, V> node = this.wrapper.getNode(posicionPrimero.getBlockNumber(), posicionPrimero.getOffset());
			primero.setNode(node);
		}
		
		return primero;
	}
	
	@Override
	protected void setFirst(Reference<K, V> first) throws BPlusTreeException {
		super.setFirst(first);
		
		DiskReference<K, V> primero = (DiskReference<K, V>) first;
		RecordPosition posicionPrimero = primero.getRecordPosition();
		
		this.wrapper.setFirstNodePosition(posicionPrimero);
	}

	@Override
	public void close() throws BPlusTreeException{

		this.wrapper.close();
		
	}
	
	public Iterator<K> iterator(){
		return new DiskBPlusTreeIterator();
	}
	
	protected class DiskBPlusTreeIterator extends BPlusTreeIterator{
		
		Set<K> keys;
		Boolean hasNext;
		LeafBPlusTreeNode<K, V> leafNodeActual;
		Iterator<K> leafIterator;
		
		protected DiskBPlusTreeIterator(){
			try {
				leafNodeActual = (LeafBPlusTreeNode<K, V>)getFirst().getNode();
				keys = leafNodeActual.getDatos().keySet();
				leafIterator = keys.iterator();
				hasNext = true;
			} catch (Exception e) {
				hasNext = false;
			}
		}

		@Override
		public boolean hasNext() {
			return hasNext;
		}

		@Override
		public K next() {
			K returnValue = null;
			
			if(!leafIterator.hasNext()){
				Reference<K, V> leafReference = leafNodeActual.getNext();
				if(leafReference != null){
					try {
						leafNodeActual = (LeafBPlusTreeNode<K, V>) leafReference.getNode();
						keys = leafNodeActual.getDatos().keySet();
						leafIterator = keys.iterator();
						hasNext = leafIterator.hasNext();
					} catch (BPlusTreeException e) {
						hasNext = false;
					}
				}else{
					hasNext = false;
				}
			}
			
			if(this.hasNext()){
				returnValue = leafIterator.next();
			}
			
			return returnValue;
		}

		@Override
		public void remove() {
			super.remove();
		}
		
		
		
	}

}
